Skip to content

feat(local): add local dev server for capturing SDK events#888

Merged
MathurAditya724 merged 49 commits into
mainfrom
feat/local-spotlight-command
May 21, 2026
Merged

feat(local): add local dev server for capturing SDK events#888
MathurAditya724 merged 49 commits into
mainfrom
feat/local-spotlight-command

Conversation

@MathurAditya724
Copy link
Copy Markdown
Member

@MathurAditya724 MathurAditya724 commented Apr 29, 2026

Summary

Adds sentry local, a local development server that captures Sentry SDK envelopes and tails errors, traces, and logs to the terminal in real time.

Two subcommands:

  • sentry local serve (default) — starts the server and tails events. If a server is already running on the port, attaches as an SSE consumer instead.
  • sentry local run -- <cmd> — runs a command with SENTRY_SPOTLIGHT injected so the SDK auto-sends envelopes to the local server. No code changes needed.

What's new

  • src/commands/local/serve.ts — the server command (default). Flags: --port/-p, --host/-H, --quiet/-q, --filter/-f.
  • src/commands/local/run.ts — child process wrapper. Injects SENTRY_SPOTLIGHT, NEXT_PUBLIC_SENTRY_SPOTLIGHT, SENTRY_TRACES_SAMPLE_RATE=1.
  • src/commands/local/index.ts — route map with serve (default) and run.
  • package.json — adds hono, @hono/node-server, @spotlightjs/spotlight as devDependencies.
  • docs/src/fragments/commands/local.md — examples, env var table, endpoint reference.

Key behaviors

  • Connect-to-existing: probes /health on startup — attaches as SSE consumer if a server is already running.
  • Port retry with backoff: 3 retries with 5s delay on EADDRINUSE.
  • Last-Event-ID reconnection: SSE subscribers can resume from where they left off.
  • ANSI sanitization: strips escape sequences from envelope content before rendering.
  • Body size guard: rejects payloads over 10 MB (413).

Endpoints (serve mode)

Method Path Description
POST /stream Envelope ingest
POST /api/{projectId}/envelope/ Sentry SDK ingest path
GET /stream SSE feed
GET /health Liveness check

Adds 'sentry local', a long-running command that starts a minimal Hono
HTTP server wire-compatible with the Spotlight sidecar protocol. The
server uses @spotlightjs/spotlight/sdk's createSpotlightBuffer +
pushToSpotlightBuffer helpers to ingest envelopes from any Sentry SDK
running in the user's dev stack and tails them to the terminal.

Endpoints exposed:
  POST /stream                          - Spotlight ingest
  POST /api/{projectId}/envelope/       - Sentry SDK ingest path
  GET  /stream                          - SSE feed for the Spotlight overlay
  GET  /health                          - liveness check

Why a thin in-tree server instead of spawning npx @spotlightjs/spotlight:
the SDK helpers give us decompression + lazy parsing for free while
keeping the surface focused on a CLI-friendly tail UX, and bundling
through esbuild keeps the published binary self-contained per the
no-runtime-dependencies rule.

The command runs without auth (it's a local dev tool) and shuts down
gracefully on SIGINT/SIGTERM, force-closing keep-alive connections so
SSE subscribers don't block exit.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 29, 2026

PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://cli.sentry.dev/_preview/pr-888/

Built to branch gh-pages at 2026-05-21 12:12 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 29, 2026

Codecov Results 📊

7137 passed | Total: 7137 | Pass Rate: 100% | Execution Time: 0ms

📊 Comparison with Base Branch

Metric Change
Total Tests 📈 +62
Passed Tests 📈 +62
Failed Tests
Skipped Tests

All tests are passing successfully.

❌ Patch coverage is 50.75%. Project has 15233 uncovered lines.
❌ Project coverage is 76.23%. Comparing base (base) to head (head).

Files with missing lines (3)
File Patch % Lines
src/commands/local/server.ts 26.71% ⚠️ 321 Missing
src/lib/formatters/local.ts 72.34% ⚠️ 78 Missing
src/commands/local/run.ts 78.46% ⚠️ 28 Missing
Coverage diff
@@            Coverage Diff             @@
##          main       #PR       +/-##
==========================================
- Coverage    76.62%    76.23%    -0.39%
==========================================
  Files          324       328        +4
  Lines        63171     64072      +901
  Branches         0         0         —
==========================================
+ Hits         48396     48839      +443
- Misses       14775     15233      +458
- Partials         0         0         —

Generated by Codecov Action

Comment thread src/commands/local.ts Outdated
Replace the minimal 'timestamp • type' one-liner with rich formatted
output that shows actual event content: error type/message with stack
location, transaction name/op/duration/span count, and log messages
with attributes. Uses the CLI's own color system since Spotlight's
humanFormatters aren't publicly exported from the package.
Adds a repeatable --filter flag that accepts error, transaction, or log.
When set, only matching envelope items are rendered in the tail output;
non-matching items are silently dropped. No filter = show everything.

Usage:
  sentry local -f error            # errors only
  sentry local -f error -f log     # errors and logs
  sentry local -f transaction      # transactions only
Three fixes based on audit against Spotlight's reference implementation:

1. Signal handling: process.once -> process.on so the 'second signal =
   force exit' code path is reachable (process.once unregisters after
   the first signal, making the shuttingDown check dead code).

2. SSE format: match the Spotlight protocol so the overlay UI works.
   - event name is the content type (not 'envelope')
   - id field is the Spotlight-assigned envelope UUID
   - data is the parsed envelope JSON (not base64-encoded raw bytes)
   - Last-Event-ID reconnection is now supported

3. Browser SDK: detect sendBeacon() payloads (Content-Type: text/plain
   with sentry_client query param) and override to the canonical
   application/x-sentry-envelope, matching Spotlight's workaround.
The --open flag opened the raw SSE endpoint in a browser, which just
shows streaming text — not useful without the Spotlight overlay UI.
Removed it and updated the fragment docs to document the new
pretty-print tail output and --filter flag instead.
Comment thread src/commands/local.ts Outdated
- Remove logger tag so 'local <timestamp>' no longer clutters every line
- Remove 'Spotlight sidecar' wording, use 'Listening on <url>' instead
- Remove endpoint listing and DSN instructions from banner
- Add Spotlight docs link for getting started
- Auto-increment port on EADDRINUSE (up to 10 attempts)
Comment thread src/commands/local/server.ts Outdated
Comment thread src/commands/local/server.ts
@MathurAditya724 MathurAditya724 changed the title feat(local): add command to run a local Spotlight sidecar feat(local): add command to run a local Spotlight server May 15, 2026
- Remove pointless `log = logger` alias; use `logger` directly
- Strip narrating comments that restate the code
- Rename buildSidecarApp → buildApp; drop remaining 'sidecar' references
- Make onEnvelope callback optional instead of passing noop
- Remove redundant type annotation on activeFilters
Startup banner keeps logger.info (shows ℹ icon), while tail output
and shutdown messages use logger.log (no icon prefix).
Comment thread src/commands/local.ts Outdated
The wildcard origin allowed any webpage to connect to the SSE stream
and exfiltrate envelope data. Restrict to localhost/127.0.0.1 origins
which is sufficient for local dev stacks (Vite, Next, Astro, etc.).
- Fix subscription leak: merge dual stream.onAbort() into one callback
  so unsubscribe and promise resolution both fire on disconnect
- Sanitize envelope content with stripAnsi() before rendering to
  terminal to prevent ANSI escape injection from crafted payloads
- Add 10 MB body size guard on ingest to reject oversized payloads
  (returns 413)
Comment thread src/commands/local/server.ts
- Type labels: uppercase, bracketed, padded — [ERROR]   [TRACE]   [INFO]
- Source labels: uppercase, bracketed, padded — [SERVER]  [BROWSER] [MOBILE]
- Source colors: match Spotlight Sentinel theme (mobile=blue)
- Log attributes: per-attribute brackets [key=value] [key=value]
- Update docs fragment example output to match
Comment thread src/commands/local.ts Outdated
exception.values is ordered oldest→newest per the Sentry protocol,
so values[0] is the root cause. Use .at(-1) to display the outermost
exception, matching Sentry UI and Spotlight behavior.
@MathurAditya724
Copy link
Copy Markdown
Member Author

fix-ci: attempt 1 — flaky property test colorizeSql > stripping ANSI preserves original text content hit seed 546244852 which generated "by a". The @sentry/sqlish parser uppercases SQL keywords (byBY), so stripAnsi(colorizeSql("by a")) returns "BY a""by a". The test's identifierArb can generate lowercase strings that happen to be SQL keywords. Fixing the assertion to compare case-insensitively.

@sentry/sqlish uppercases SQL keywords (e.g. "by" → "BY"), so
identifiers that happen to match keywords fail the strict equality
check. Compare lowercased strings instead.
- On startup, probe the target port for an existing Spotlight server.
  If one is running, attach as an SSE consumer instead of starting a
  duplicate server. Uses fetch-based SSE parsing since Bun lacks
  global EventSource.
- Last-Event-ID reconnection already supported via the Spotlight SDK's
  subscribe(callback, lastEventId) parameter.
- Port retry now uses 3 retries with 5s backoff (matching Spotlight)
  instead of 10 sequential port increments.
Comment thread src/commands/local/server.ts Outdated
Comment thread src/commands/local.ts Outdated
…rigins

Returning a mismatched string still worked (browser blocks on mismatch)
but returning null correctly omits the Access-Control-Allow-Origin
header per Hono's CORS middleware API.
Comment thread src/commands/local.ts Outdated
Comment thread src/commands/local/server.ts
@MathurAditya724
Copy link
Copy Markdown
Member Author

fix-ci: attempt 1 — biome formatting issue in src/commands/local.ts, ternary needs parens

Comment thread src/commands/local/server.ts Outdated
Comment thread src/commands/local/server.ts Outdated
Comment thread src/commands/local/run.ts Outdated
Comment thread src/commands/local/server.ts Outdated
Comment thread src/commands/local/server.ts Outdated
… control chars

- run: set process.exitCode directly instead of EXIT.GENERAL so
  callers can distinguish the child's error type.
- server: validate content-encoding header against known values
  before passing to pushToSpotlightBuffer.
- sanitize: strip C0 control characters (BEL, BS, etc.) in addition
  to ANSI escapes and newlines.
- waitForShutdown: remove SIGINT/SIGTERM listeners after the first
  signal fires, preventing handler accumulation.
- consumer mode: wrap consumeSSE in try/finally to remove signal
  handlers on both normal exit and error paths.
Comment thread src/commands/local/run.ts
Comment thread src/commands/local/server.ts
Comment thread src/commands/local/run.ts
Comment thread src/lib/formatters/local.ts Outdated
tryListen now reads the OS-assigned port from server.address() instead
of returning the original argument. run.ts builds spotlightUrl after
the server starts so the child process gets the correct URL.
…el colors

- run: revert to throwing CliError with the child's actual exit code
  instead of setting process.exitCode, matching test expectations.
- formatType: use lowercase level for LEVEL_COLORS lookup so
  uppercase levels like 'INFO' still get colored output.
Comment thread src/lib/formatters/local.ts Outdated
Comment thread src/commands/local/server.ts
Comment thread src/lib/formatters/local.ts
… separator

- Use ...args rest parameter (Stricli passes variadic positionals
  as individual params, not an array)
- Strip leading '--' separator that Stricli passes through
Without signal forwarding, SIGTERM sent to the parent doesn't reach
the grandchild process (e.g. bun run app.ts), causing the run command
to hang on shutdown.
Comment thread src/commands/local/run.ts
Comment thread src/commands/local/server.ts
@MathurAditya724
Copy link
Copy Markdown
Member Author

MathurAditya724 commented May 21, 2026

fix-ci budget exhausted (3 prior attempts), but this failure was already addressed in ff3c8b8 (pushed before this CI notification arrived). The test was passing args as an array instead of rest params — fixed by spreading them individually. CI is running on the fix now.

…n type

The buildCommand wrapper consumes the async generator internally, so
loader() returns a plain async function. Tests must spread args as
individual parameters (matching Stricli's variadic positional convention)
and await the Promise directly.
Comment thread src/commands/local/run.ts
The reverse order can prevent the close callback from firing when
active SSE connections exist, causing the shutdown promise to hang.
Comment thread src/commands/local/server.ts Outdated
Comment thread src/lib/formatters/local.ts
- CORS: add [::1] to LOCALHOST_ORIGIN_RE so IPv6 dev stacks pass
  preflight.
- formatSingleLog: guard against null attribute entries before
  accessing v.value.
- run.ts: remove unused biome-ignore suppression.
Comment thread src/lib/formatters/local.ts Outdated
Extend sanitize() to cover the C1 range (0x80-0x9F) which includes
raw 8-bit CSI/OSC/DCS introducers, and collapse NEL (U+0085) as a
line break.
@MathurAditya724
Copy link
Copy Markdown
Member Author

the Secret Scan failure is a false positive — it's flagging the pnpm patch hash for @sentry/core (2351f28c53bf...) as a "SentryToken". this hash already exists on main in pnpm-lock.yaml and isn't an actual secret. this is an org-level workflow issue, not something in the PR's code changes.

Comment thread src/commands/local/server.ts
Comment thread src/commands/local/server.ts
Comment thread src/lib/formatters/local.ts Outdated
Comment thread src/commands/local/server.ts
…ze()

Add a pass to remove bidi marks (U+200E-200F, U+202A-202E, U+2066-2069)
that can reorder terminal output rendering.
@MathurAditya724 MathurAditya724 merged commit a3396d5 into main May 21, 2026
29 checks passed
@MathurAditya724 MathurAditya724 deleted the feat/local-spotlight-command branch May 21, 2026 13:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant